package com.rtsffm.rtd.action.controller.swing;

import com.rtsffm.rtd.action.controller.ActionService;
import com.rtsffm.rtd.action.controller.ActionServiceHook;

//~--- JDK imports ------------------------------------------------------------

import java.awt.event.ActionEvent;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;

import static javax.swing.JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;

//~--- classes ----------------------------------------------------------------

public class SwingActionServiceHook implements ActionServiceHook {
    private final ActionService            actionService;
    private final Map<String, MyKeyStroke> keyStrokeMap;
    private final Lock                     readLock, writeLock;

    //~--- constructors -------------------------------------------------------

    public SwingActionServiceHook(ActionService service, Map<String, KeyStroke> hotKeyStrokeMap) {
        ReadWriteLock lock = new ReentrantReadWriteLock();

        this.actionService = service;
        this.readLock      = lock.readLock();
        this.writeLock     = lock.writeLock();
        keyStrokeMap       = new HashMap<String, SwingActionServiceHook.MyKeyStroke>();

        for (Entry<String, KeyStroke> entry : hotKeyStrokeMap.entrySet()) {
            keyStrokeMap.put(entry.getKey(), new MyKeyStroke(entry.getKey(), entry.getValue()));
        }
    }

    //~--- methods ------------------------------------------------------------

    @Override
    public void newAction(final String actionId, final Object actionProvider) {
        if (actionProvider instanceof JComponent) {
            JComponent comp          = (JComponent) actionProvider;
            ActionMap  compActionMap = comp.getActionMap();
            ActionMap  actionMap     = new ActionMap();
            InputMap   inpMap;

            readLock.lock();

            try {
                inpMap = new SingleActionInputMap(comp.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT), actionId,
                                                  keyStrokeMap.get(actionId));
            } finally {
                readLock.unlock();
            }

            actionMap.put(actionId, new AbstractAction() {
                private static final long serialVersionUID = 1L;

                @Override
                public void actionPerformed(ActionEvent event) {
                    actionService.performAction(actionId, actionProvider, event);
                }
            });

            actionMap.setParent(compActionMap);

            comp.setActionMap(actionMap);
            comp.setInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, inpMap);
        }
    }

    public void updateServiceHook(Map<String, KeyStroke> deltaHotKeyStrokeMap) {
        writeLock.lock();

        try {
            for (Entry<String, KeyStroke> entry : deltaHotKeyStrokeMap.entrySet()) {
                String      actionId = entry.getKey();
                KeyStroke   ks       = entry.getValue();
                MyKeyStroke myStroke;

                if ((myStroke = keyStrokeMap.get(actionId)) != null) {
                    myStroke.keyStroke = ks;
                } else {
                    keyStrokeMap.put(actionId, new MyKeyStroke(actionId, ks));
                }
            }
        } finally {
            writeLock.unlock();
        }
    }

    //~--- inner classes ------------------------------------------------------

    private class MyKeyStroke {
        private Object    actionId;
        private KeyStroke keyStroke;

        //~--- constructors ---------------------------------------------------

        public MyKeyStroke(Object action, KeyStroke ks) {
            actionId  = action;
            keyStroke = ks;
        }

        //~--- methods --------------------------------------------------------

        @Override
        public String toString() {
            return actionId + " ==> " + keyStroke + ";";
        }
    }


    private class SingleActionInputMap extends InputMap {
        private static final long serialVersionUID = 1L;

        //~--- fields ---------------------------------------------------------

        private AtomicBoolean isValid = new AtomicBoolean(true);
        private Object        actionId;
        private MyKeyStroke   myStroke;

        //~--- constructors ---------------------------------------------------

        SingleActionInputMap(InputMap parent, String actionId_, MyKeyStroke keyStroke_) {
            if ((parent == null) || (actionId_ == null) || (keyStroke_ == null)) {
                throw new RuntimeException("'null' value not allowed");
            }

            setParent(parent);
            actionId = actionId_;
            myStroke = keyStroke_;
        }

        //~--- methods --------------------------------------------------------

        @Override
        public KeyStroke[] allKeys() {
            if (isValid.get()) {
                return getParent().allKeys();
            }

            Set<KeyStroke> pkeys = new HashSet<KeyStroke>(Arrays.asList(getParent().allKeys()));

            pkeys.add(myStroke.keyStroke);

            return pkeys.toArray(new KeyStroke[] {});
        }

        @Override
        public int size() {
            return isValid.get()
                   ? 1
                   : 0;
        }

        @Override
        public void remove(KeyStroke key) {
            if (key == myStroke.keyStroke) {
                isValid.set(false);
            }
        }

        @Override
        public KeyStroke[] keys() {
            return isValid.get()
                   ? new KeyStroke[] { myStroke.keyStroke }
                   : null;
        }

        @Override
        public void clear() {
            isValid.set(false);
        }

        @Override
        public void put(KeyStroke keyStroke, Object actionMapKey) {
            if (keyStroke == myStroke.keyStroke) {
                myStroke = new MyKeyStroke(actionMapKey, keyStroke);
                actionId = actionMapKey;
                isValid.set(true);
            } else {
                getParent().put(keyStroke, actionMapKey);
            }
        }

        //~--- get methods ----------------------------------------------------

        @Override
        public Object get(KeyStroke keyStroke) {
            if (isValid.get()) {
                return getParent().get(keyStroke);
            }

            if (keyStroke == myStroke.keyStroke) {
                return actionId;
            } else {
                return getParent().get(keyStroke);
            }
        }
    }
}
